/*
 * Copyright 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.credentials.playservices.controllers.identityauth.createpublickeycredential

import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.CancellationSignal
import android.os.Handler
import android.os.Looper
import android.os.ResultReceiver
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.credentials.CreateCredentialResponse
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.CreatePublicKeyCredentialResponse
import androidx.credentials.CredentialManagerCallback
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.domerrors.EncodingError
import androidx.credentials.exceptions.domerrors.UnknownError
import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException
import androidx.credentials.playservices.CredentialProviderPlayServicesImpl
import androidx.credentials.playservices.controllers.CredentialProviderBaseController
import androidx.credentials.playservices.controllers.CredentialProviderController
import androidx.credentials.playservices.controllers.identityauth.HiddenActivity
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.fido.Fido
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions
import java.util.concurrent.Executor
import org.json.JSONException

/** A controller to handle the CreatePublicKeyCredential flow with play services. */
@Suppress("deprecation")
internal class CredentialProviderCreatePublicKeyCredentialController(private val context: Context) :
    CredentialProviderController<
        CreatePublicKeyCredentialRequest,
        PublicKeyCredentialCreationOptions,
        PublicKeyCredential,
        CreateCredentialResponse,
        CreateCredentialException,
    >(context) {

    /** The callback object state, used in the protected handleResponse method. */
    @VisibleForTesting
    private lateinit var callback:
        CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>

    /** The callback requires an executor to invoke it. */
    @VisibleForTesting private lateinit var executor: Executor

    /**
     * The cancellation signal, which is shuttled around to stop the flow at any moment prior to
     * returning data.
     */
    @VisibleForTesting private var cancellationSignal: CancellationSignal? = null

    private val resultReceiver =
        object : ResultReceiver(Handler(Looper.getMainLooper())) {
            public override fun onReceiveResult(resultCode: Int, resultData: Bundle) {
                if (
                    maybeReportErrorFromResultReceiver(
                        resultData,
                        CredentialProviderBaseController.Companion::
                            createCredentialExceptionTypeToException,
                        executor = executor,
                        callback = callback,
                        cancellationSignal,
                    )
                )
                    return
                handleResponse(
                    resultData.getInt(ACTIVITY_REQUEST_CODE_TAG),
                    resultCode,
                    resultData.getParcelable(RESULT_DATA_TAG),
                )
            }
        }

    override fun invokePlayServices(
        request: CreatePublicKeyCredentialRequest,
        callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>,
        executor: Executor,
        cancellationSignal: CancellationSignal?,
    ) {
        this.cancellationSignal = cancellationSignal
        this.callback = callback
        this.executor = executor
        val fidoRegistrationRequest: PublicKeyCredentialCreationOptions
        try {
            fidoRegistrationRequest = this.convertRequestToPlayServices(request)
        } catch (e: JSONException) {
            cancelOrCallbackExceptionOrResult(cancellationSignal) {
                this.executor.execute { this.callback.onError(JSONExceptionToPKCError(e)) }
            }
            return
        } catch (t: Throwable) {
            cancelOrCallbackExceptionOrResult(cancellationSignal) {
                this.executor.execute {
                    this.callback.onError(CreateCredentialUnknownException(t.message))
                }
            }
            return
        }

        if (CredentialProviderPlayServicesImpl.cancellationReviewer(cancellationSignal)) {
            return
        }
        Fido.getFido2ApiClient(context)
            .getRegisterPendingIntent(fidoRegistrationRequest)
            .addOnSuccessListener { result: PendingIntent ->
                if (CredentialProviderPlayServicesImpl.cancellationReviewer(cancellationSignal)) {
                    return@addOnSuccessListener
                }
                val hiddenIntent = Intent(context, HiddenActivity::class.java)
                generateHiddenActivityIntent(
                    resultReceiver,
                    hiddenIntent,
                    CREATE_PUBLIC_KEY_CREDENTIAL_TAG,
                )
                hiddenIntent.putExtra(EXTRA_FLOW_PENDING_INTENT, result)
                try {
                    context.startActivity(hiddenIntent)
                } catch (_: Exception) {
                    cancelOrCallbackExceptionOrResult(cancellationSignal) {
                        this.executor.execute {
                            this.callback.onError(
                                CreateCredentialUnknownException(
                                    ERROR_MESSAGE_START_ACTIVITY_FAILED
                                )
                            )
                        }
                    }
                }
            }
            .addOnFailureListener { e ->
                val createException = fromIntentRequestException(e)
                cancelOrCallbackExceptionOrResult(cancellationSignal) {
                    this.executor.execute { this.callback.onError(createException) }
                }
            }
    }

    private fun fromIntentRequestException(e: Throwable): CreateCredentialException {
        var errName = CREATE_UNKNOWN
        if (e is ApiException && e.statusCode in retryables) {
            errName = CREATE_INTERRUPTED
        }
        return createCredentialExceptionTypeToException(
            errName,
            "During create public key credential, fido registration " + "failure: ${e.message}",
        )
    }

    internal fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
        if (uniqueRequestCode != CONTROLLER_REQUEST_CODE) {
            Log.w(
                TAG,
                "Returned request code " +
                    "$CONTROLLER_REQUEST_CODE does not match what was given $uniqueRequestCode",
            )
            return
        }
        if (
            maybeReportErrorResultCodeCreate(
                resultCode,
                { s, f -> cancelOrCallbackExceptionOrResult(s, f) },
                { e -> this.executor.execute { this.callback.onError(e) } },
                cancellationSignal,
            )
        )
            return
        val bytes: ByteArray? = data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
        if (bytes == null) {
            if (CredentialProviderPlayServicesImpl.cancellationReviewer(cancellationSignal)) {
                return
            }
            this.executor.execute {
                this.callback.onError(
                    CreatePublicKeyCredentialDomException(
                        UnknownError(),
                        "Upon handling create public key credential response, fido module giving" +
                            " null bytes indicating internal error",
                    )
                )
            }
            return
        }
        val cred: PublicKeyCredential = PublicKeyCredential.deserializeFromBytes(bytes)
        val exception =
            PublicKeyCredentialControllerUtility.publicKeyCredentialResponseContainsError(cred)
        if (exception != null) {
            cancelOrCallbackExceptionOrResult(cancellationSignal) {
                executor.execute { callback.onError(exception) }
            }
            return
        }
        try {
            val response = this.convertResponseToCredentialManager(cred)
            cancelOrCallbackExceptionOrResult(cancellationSignal) {
                this.executor.execute { this.callback.onResult(response) }
            }
        } catch (e: JSONException) {
            cancelOrCallbackExceptionOrResult(cancellationSignal) {
                executor.execute {
                    callback.onError(
                        CreatePublicKeyCredentialDomException(EncodingError(), e.message)
                    )
                }
            }
        } catch (t: Throwable) {
            cancelOrCallbackExceptionOrResult(cancellationSignal) {
                executor.execute {
                    callback.onError(
                        CreatePublicKeyCredentialDomException(UnknownError(), t.message)
                    )
                }
            }
        }
    }

    @VisibleForTesting
    public override fun convertRequestToPlayServices(
        request: CreatePublicKeyCredentialRequest
    ): PublicKeyCredentialCreationOptions {
        return PublicKeyCredentialControllerUtility.convert(request, context)
    }

    @VisibleForTesting
    public override fun convertResponseToCredentialManager(
        response: PublicKeyCredential
    ): CreateCredentialResponse {
        try {
            return CreatePublicKeyCredentialResponse(response.toJson())
        } catch (t: Throwable) {
            throw CreateCredentialUnknownException(
                "The PublicKeyCredential response json " +
                    "had an unexpected exception when parsing: ${t.message}"
            )
        }
    }

    private fun JSONExceptionToPKCError(
        exception: JSONException
    ): CreatePublicKeyCredentialDomException {
        val myCopy: String? = exception.message
        if (myCopy != null && myCopy.length > 0) {
            return CreatePublicKeyCredentialDomException(EncodingError(), myCopy)
        }
        return CreatePublicKeyCredentialDomException(EncodingError(), "Unknown error")
    }

    companion object {
        private const val TAG = "CreatePublicKey"

        /**
         * Factory method for [CredentialProviderCreatePublicKeyCredentialController].
         *
         * @param context the calling context for this controller
         * @return a credential provider controller for CreatePublicKeyCredential
         */
        @JvmStatic
        fun getInstance(context: Context): CredentialProviderCreatePublicKeyCredentialController {
            return CredentialProviderCreatePublicKeyCredentialController(context)
        }
    }
}
